feat: AsyncClient.aclose() + modern-di setup-friction recipe#29
Merged
Conversation
…aclose Setup-friction recipe for wiring `httpware.AsyncClient` into a `modern-di` container. Documents two non-obvious moments real users will hit: - Lifecycle finalizer: bridging AsyncClient's context-manager teardown with modern-di's Factory(cache_settings=CacheSettings(finalizer=...)) via the unbound `AsyncClient.aclose` (modern-di auto-awaits async coroutine functions; a `lambda c: c.aclose()` would NOT work because iscoroutinefunction returns False on lambdas). - Multi-backend collision: two Factory(creator=AsyncClient,...) providers raise DuplicateProviderTypeError at container construction (verified against modern_di/registries/providers_registry.py:42-44). Idiomatic fix is a per-backend wrapper subclass so each Factory has a distinct bound_type. While drafting, found that `httpware.AsyncClient` exposes __aenter__ / __aexit__ but no standalone aclose() — even though the CLAUDE.md naming convention names it as if it exists. Spec scopes adding aclose() into the same PR so the recipe finalizer can be the clean `AsyncClient.aclose` instead of a workaround calling __aexit__ directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lose Three tasks: (1) TDD aclose() with two new tests, (2) write the recipe page + mkdocs nav + index back-link, (3) verification (round-trip finalizer call, multi-backend distinct providers, exact-name-of-error gate for the documented DuplicateProviderTypeError). Plan corrects a spec discrepancy: lifecycle tests go in tests/test_client_lifecycle.py, not the non-existent tests/test_client.py. Test names tightened to match the existing test_aexit_* convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the body of __aexit__: closes the underlying httpx2 client iff we own it and it isn't already closed. Idempotent. Use case: DI containers, background workers, anything not request-shaped that can't lean on `async with`. Aligns the library with its own CLAUDE.md naming convention which already names aclose() as the sole a-prefixed method exception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linear-narrative walk-through of wiring AsyncClient into a modern-di container: minimal Factory + finalizer → multi-backend type collision (DuplicateProviderTypeError) → per-backend wrapper-subclass fix → middleware in kwargs. Adds a new top-level "Recipes" nav section (single item for now) and one back-link from the index's "Where to go next". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end verification against the published modern-di surfaced that the actual exception message reads "Provider is duplicated by type <class 'httpware.client.AsyncClient'>. To resolve this issue: ..." — not the shorter "AsyncClient is already registered" the recipe showed. Module path and class name were already correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
httpware.AsyncClient.aclose()— a standalone async teardown method mirroring the body of__aexit__. Idempotent, owns-client-only. Enables non-context-manager scoping (DI containers, background workers).docs/recipes/modern-di.mdwalking through wiringAsyncClientinto amodern-dicontainer's lifecycle: minimalFactory+ finalizer → multi-backendDuplicateProviderTypeError→ per-backend wrapper-subclass fix → middleware inkwargs=.Recipesnav section tomkdocs.ymland one back-link fromdocs/index.md.Why bundle the method with the docs
The recipe's finalizer reads as
finalizer=AsyncClient.aclose— a clean one-liner. Without the method, the only path wasawait c.__aexit__(None, None, None), which signals a library gap and reads as a workaround. Shipping both keeps the public surface coherent with the project's own naming convention (CLAUDE.md namesaclose()as the solea-prefixed method exception, but it didn't exist).What's deliberately NOT in this PR
aclosetest for the_owns_client=Falsebranch. The spec excluded it on the basis thattest_aexit_does_not_close_borrowed_httpx2_clientcovers the same guard expression. The final reviewer flagged this as a minor coverage gap; line coverage is 100% (the enforced gate), but branch coverage on the new method is < 100%. Happy to addtest_aclose_does_not_close_borrowed_httpx2_clientin a follow-up if desired.async defworkaround snippet alongside the "lambda doesn't work" warning in the recipe. The direct-method form is strictly better; a closure rewrite would be padding.docs/resilience.md,docs/middleware.md,docs/errors.md,docs/testing.md— those are topical reference pages and shouldn't fan into a recipe.modern-di/docs/integrations/— that's a separate PR in the sibling repo if desired.Commits
a2c1fbc05b12a2a1b9faa0142110e91892fTest plan
just test— 253 passed (251 baseline + 2 new), 100% coverage enforcedjust lint— ruff format + ruff check + ty check all cleanuv run --with mkdocs --with mkdocs-material mkdocs build --strict— clean, generatesrecipes/modern-di/index.htmlmodern-di: finalizer round-trip, multi-backend distinct providers + both finalizers fire, documentedDuplicateProviderTypeErrorraises with the exact message text the recipe showsuv run --with mkdocs --with mkdocs-material mkdocs serveand open http://127.0.0.1:8000/recipes/modern-di/🤖 Generated with Claude Code